/**
 * 
 */
package com.tkiicpp.tank.utils.dependence;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Map.Entry;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import com.tkiicpp.tank.utils.clazz.Clazz;
import com.tkiicpp.tank.utils.clazz.constant.ClassConstant;
import com.tkiicpp.tank.utils.clazz.constant.ClassConstant.ConstantType;
import com.tkiicpp.tank.utils.clazz.constant.impl.ClassRefConstant;
import com.tkiicpp.tank.utils.dependence.DependenceTree.ClassNode;
import com.tkiicpp.tank.utils.dependence.DependenceTree.JarNode;


/**
 * @author <a href="mailto:tanktong@gmail.com">Tank.Tong</a>
 * @version 1.0 2010-08-25 17:18:43
 * @since 1.0
 */
public class DependenceAnalyzer {
	
	public static DependenceTree analyzeAllPossibleDependence(String jarFileName) throws IOException {
		
		File file = new File(jarFileName);
		JarFile jarFile = new JarFile(file);
		DependenceTree tree = new DependenceTree(getJarName(jarFileName));
//		ClassLoader loader = DependenceAnalyzer.class.getClassLoader();
		URL[] urls = new URL[1]; 
	    urls[0] = new URL("file:///" + file.getAbsolutePath());
		ClassLoader loader = new URLClassLoader(urls, 
				DependenceAnalyzer.class.getClassLoader());
		
		Enumeration<JarEntry> entryEnum = jarFile.entries();
		JarEntry entry = null;
		
		while (entryEnum.hasMoreElements()) {
			entry = entryEnum.nextElement();
			if (!isClassFile(entry)) {
				continue;
			}
			
			analyzeClass(entry.getName(), loader, tree);
		}
		
		tree.link();
		return tree;
	}
	
	private static void analyzeClass(String classFileName, 
			ClassLoader loader, DependenceTree tree) throws IOException {
		
		String className = convertToClassName(classFileName);
		
		if (tree.containsClassNode(className)) {
			return;
		}
		
		Enumeration<URL> urls = loader.getResources(classFileName);
		
		URL url = null;
		String path = null;
		String jarName = null;
		Clazz clazz = null;
		JarNode jarNode = null;
		String refClassName = null;
		String depClassName = null;
		ClassNode node = null;
		InputStream in = null;
		if (!urls.hasMoreElements()) {
			tree.new ClassNode(className, tree, tree.getJarNode(null));
			return;
		}
		
		do {
			url = urls.nextElement();
			path = url.getFile();
			jarName = getJarName(path);
			in = url.openStream();
			
			clazz = Clazz.buildClazz(className, in);
			in.close();
			
			jarNode = tree.getJarNode(jarName);
			node = tree.new ClassNode(className, tree, jarNode);
			
			for (ClassConstant constant : clazz.getConstantPool().getClassConstantsByType(ConstantType.CLASS)) {
				refClassName = ((ClassRefConstant) constant).getClassName();
				
				if (isSystemClass(refClassName)) {
					continue;
				}
				
				depClassName = convertToClassName(refClassName);
				node.addDependenceClassName(depClassName);
				analyzeClass(
						convertToClassFileName(refClassName), 
						loader, tree);
			}
		} while (urls.hasMoreElements());
		
	}
	
	private static boolean isSystemClass(String className) {
		return className.startsWith("[") || className.startsWith("L");
	}
	
	private static boolean isClassFile(JarEntry entry) {
		if (null != entry && entry.getName() != null 
				&& entry.getName().endsWith(".class")) {
			return true;
		}
		
		return false;
		
	}
	
	private static String getJarName(String name) {
		int index = name.lastIndexOf('!');
		name = index == -1 ? name : name.substring(0, index);
		index = name.lastIndexOf('/');
		name = index == -1 ? name : name.substring(index + 1);
		index = name.lastIndexOf('\\');
		
		return index == -1 ? name : name.substring(index + 1);
	}
	
	private static String convertToClassFileName(String className) {
		return className.replaceAll("\\.", "/") + ".class";
	}
	
	private static String convertToClassName(String classFileName) {
		classFileName = classFileName.replaceAll("/", "\\.");
		return classFileName.endsWith(".class") 
				? classFileName.substring(0, classFileName.length() - 6) : classFileName;
	}
	
	public static void main(String[] args) throws IOException {
//		File file = new File("C:/jar/");
//		
//		if (!file.exists()) {
//			System.err.println("Not find directory " + file.getPath());
//			System.exit(-1);
//		}
//		
//		for (File subFile : file.listFiles()) {
//			if (subFile.isFile() && subFile.getName().startsWith("sg-") && subFile.getName().endsWith(".jar")) {
//				analyzeJar(subFile);
//			}
//		}
		
//		analyzeJar(new File("C:/work/workspace/Common_Dependence/workspace/commons/sg-commons-remotelogger.jar"));
		analyzeJar(new File("C:/jar/sg-party.jar"));
	}
	
	private static void analyzeJar(File jarFile) throws IOException {
		DependenceTree tree = DependenceAnalyzer.analyzeAllPossibleDependence(jarFile.getAbsolutePath());
	
		String html = printToHtml(tree);
		File file = new File(jarFile.getName().substring(0, jarFile.getName().length() - 4) + "-dependence.html");
		if (!file.exists()) {
			file.createNewFile();
		}
		
		FileOutputStream out = new FileOutputStream(file);
		out.write(html.getBytes());
		out.flush();
		out.close();
		System.out.println("Write to file " + file.getAbsolutePath());
	}
		
	private static String printToHtml(DependenceTree tree) {
		if (null == tree) {
			System.err.println("Null dependence tree.");
			return null;
		}
		
		JarNode root = tree.getRoot();
		StringBuilder s = new StringBuilder();
		printHtmlHead(s, "Dependence tree for " + root.getJarName());
		
		printBody(s, tree);
		return s.toString();
	}
	
	private static void printHtmlHead(StringBuilder s, String title) {
		s.append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n");
		s.append("<html>\n");
		s.append("<head>\n");
		s.append("<title>").append(title).append("</title>\n");
		s.append("</head>\n");
	}
	
	private static void printBody(StringBuilder s, DependenceTree tree) {
		s.append("<body>\n");
		s.append("<a name='top'>Top</a>");
		
		printAllJars(s, tree);
		
		printJarTable(s, tree.getRoot());
		
		for (JarNode jarNode : tree.getAllJarNodes().values()) {
			if (jarNode != null && jarNode.equals(tree.getRoot())) {
				continue;
			}
			
			printJarTable(s, jarNode);
		}
		
		printClassTable(s, tree);
		s.append("</body>\n");
		s.append("</html>\n");
	}
	
	private static void printAllJars(StringBuilder s, DependenceTree tree) {
		s.append("<table width='100%' border='0' cellpadding='4' cellspacing='0'>\n<tbody>\n<tr>\n");
		s.append("<th align='left'><a name='class_jar'>");
		s.append("All jars</a><th>\n");
		s.append("<th align='right'><a href='#top'>[Back to top]</a></th>\n</tr>\n</tbody>\n</table>\n");
		s.append("<table width='100%' border='1' cellpadding='4' cellspacing='0'>\n");
		s.append("<tbody>\n");
		s.append("<tr>\n<th>Jar name</th>\n</tr>\n");
		
		for (JarNode jar : tree.getAllJarNodes().values()) {
			s.append("<tr>\n<td><a href='#jar_").append(jar.getJarName()).append("'>").append(jar.getJarName()).append("</a>");
			s.append("</td>\n</tr>\n");
		}
		
		s.append("</tbody>\n</table>\n");
	}
	
	private static void printClassTable(StringBuilder s, DependenceTree tree) {
		s.append("<table width='100%' border='0' cellpadding='4' cellspacing='0'>\n<tbody>\n<tr>\n");
		s.append("<th align='left'><a name='class_all'>");
		s.append("All classes</a><th>\n");
		s.append("<th align='right'><a href='#top'>[Back to top]</a></th>\n</tr>\n</tbody>\n</table>\n");
		s.append("<table width='100%' border='1' cellpadding='4' cellspacing='0'>\n");
		s.append("<tbody>\n");
		s.append("<tr>\n<th>Class name</th>\n<th>Jar files</th>\n<th>\nDepend classes</th>\n<th>\nDepend by classes</th>\n</tr>\n");
		
		List<Entry<String, List<ClassNode>>> sortedEntry = 
			new ArrayList<Entry<String, List<ClassNode>>>(tree.getAllClassNodes().entrySet());
		
		Collections.sort(sortedEntry, new Comparator<Entry<String,List<ClassNode>>>() {

			@Override
			public int compare(Entry<String, List<ClassNode>> o1,
					Entry<String, List<ClassNode>> o2) {
				return o1.getKey().compareTo(o2.getKey());
			}
		});
		
		for (Entry<String, List<ClassNode>> entry : sortedEntry) {
			s.append("<tr>\n<td>\n<a name='class_").append(entry.getKey()).append("'>");
			s.append(entry.getKey()).append("</a></td>\n");
			s.append("<td>\n");
			String jarName = null;
			for (ClassNode node : entry.getValue()) {
				jarName = node.getJarNode() == null ? "NULL" : node.getJarNode().getJarName();
				s.append("<a href='#jar_").append(jarName).append("'>");
				s.append(jarName).append("</a>, ");
			}
			s.deleteCharAt(s.length() - 2);
			s.append("</td>\n");
			s.append("<td>\n");
			for (ClassNode dep : entry.getValue()) {
				s.append("<a href='#class_").append(dep.getClassName()).append("'>");
				s.append(dep.getClassName()).append("</a>, ");
			}
			s.deleteCharAt(s.length() - 2);
			s.append("</td>\n");
			s.append("<td>\n");
			for (ClassNode dep : entry.getValue().get(0).getDependeByClassNodes()) {
				s.append("<a href='#class_").append(dep.getClassName()).append("'>");
				s.append(dep.getClassName()).append("</a>, ");
			}
			s.deleteCharAt(s.length() - 2);
			s.append("</td>\n</tr>\n");
		}
		
		s.append("</tbody>\n</table>\n<p></p>\n");
	}
	
	private static void printJarTable(StringBuilder s, JarNode jarNode) {
		String jarName = null == jarNode ? "NULL" : jarNode.getJarName();
		s.append("<table width='100%' border='0' cellpadding='4' cellspacing='0'>\n<tbody>\n<tr>\n");
		s.append("<th align='left'><a name='jar_").append(jarName).append("'>");
		s.append(jarName).append("</a><th>\n");
		s.append("<th align='right'><a href='#top'>[Back to top]</a></th>\n</tr>\n</tbody>\n</table>\n");
		s.append("<table width='100%' border='1' cellpadding='4' cellspacing='0'>\n");
		s.append("<tbody>\n");
		s.append("<tr>\n<th>Jar name</th>\n<th>Dependence class names</th>\n</tr>\n");
		
		List<Entry<JarNode, Set<ClassNode>>> sortedEntry = 
			new ArrayList<Entry<JarNode, Set<ClassNode>>>(jarNode.directDependenceJars().entrySet());
		
		Collections.sort(sortedEntry, new Comparator<Entry<JarNode, Set<ClassNode>>>() {

			@Override
			public int compare(Entry<JarNode, Set<ClassNode>> o1,
					Entry<JarNode, Set<ClassNode>> o2) {
				Iterator<ClassNode> nodes1 = o1.getValue().iterator();
				Iterator<ClassNode> nodes2 = o2.getValue().iterator();
				int result = 0;
				
				while (nodes1.hasNext()) {
					//第一个多的时候认为第一个大
					if (!nodes2.hasNext()) {
						return 1;
					}
					
					result = nodes1.next().getClassName().compareTo(nodes2.next().getClassName());
					
					if (result != 0) {
						return result;
					}
				}
				
				if (nodes2.hasNext()) {
					return -1;
				}
				
				return 0;
			}
		});
		
		for (Entry<JarNode, Set<ClassNode>> entry : sortedEntry) {
			if (entry.getKey() != null 
					&& (entry.getKey().equals(jarNode) || entry.getKey().getJarName().equals("rt.jar"))) {
				continue;
			}
			
			printJarRow(s, entry);
		}
		s.append("</tbody>\n</table>\n<p></p>\n");
	}
	
	private static void printJarRow(StringBuilder s, Entry<JarNode, Set<ClassNode>> entry) {
		String jarName = entry.getKey() == null ? "NULL" : entry.getKey().getJarName();
		s.append("<tr>\n<td><a href='#jar_").append(jarName).append("'>").append(jarName).append("</a></td>\n");
		s.append("<td>");
		for (ClassNode classNode : entry.getValue()) {
			s.append("<a href='#class_").append(classNode.getClassName()).append("'>");
			s.append(classNode.getClassName()).append("</a>, ");
		}
		s.deleteCharAt(s.length() - 2);
		s.append("</td>\n</tr>");
	}
	
	private static void printDirectDependenceJars(JarNode jarNode, Set<JarNode> jarNodes) {
		if (jarNodes.contains(jarNode)) {
			return;
		}
		
//		System.out.print(jarNode.getJarName() + "[");
//		for (Entry<JarNode, List<ClassNode>> entry : jarNode.directDependenceJars().entrySet()) {
//			System.out.print(jar.getJarName() + ",");
//		}
//		System.out.println("]");
//		
//		jarNodes.add(jarNode);
//		
//		for (JarNode jar : jarNode.directDependenceJars()) {
//			printDirectDependenceJars(jar, jarNodes);
//		}
	}
	
	private static void printDependenceChain(JarNode jarNode, Set<JarNode> jarNodes) {
		System.out.print(jarNode.getJarName());
		
		if (jarNodes.contains(jarNode)) {
			System.out.print("(Cycle!)");
			return;
		}
		
		jarNodes.add(jarNode);
		System.out.print(" , ");
//		for (JarNode jar : jarNode.directDependenceJars()) {
//			printDependenceChain(jar, jarNodes);
//		}
	}

}
